import {DisplayDifferential} from "../diff/differential";
import {DisplayEvent, IEvent, ProcessEvent, MouseEvent, MetadataResponse} from "../event-types";
import {contains, containsPoint} from "../helper";


export class ProcessEventDerivation {

    public deriveProcessEvents(events: IEvent[], differential: DisplayDifferential[], metadata: MetadataResponse) {
        const selectionThroughDifferential = this.selectionThroughDifferential(differential);
        const memberSelectionThroughZone = this.memberSelectionThroughZone(differential, events, selectionThroughDifferential);
        const dimensionAndHierarchySelection = this.dimensionAndHierarchySelection(events, metadata);

        const selectionEvents = selectionThroughDifferential

            .concat(memberSelectionThroughZone)
            .concat(dimensionAndHierarchySelection);

        const deselectionEvents = this.deselectionEvents(selectionEvents);
        return selectionEvents.concat(deselectionEvents)
            .sort((a, b) => a.timestamp - b.timestamp);
    }

    private selectionThroughDifferential(differential: DisplayDifferential[]) {
        return differential.map((diff) => {
            return diff.diffs.filter(d => d.type === 'update' &&
                d.current?.metadataType &&
                d.current.checked !== undefined &&
                d.next?.checked !== undefined &&
                d.current.checked !== d.next?.checked &&
                d.current.metadataType !== 'level')

                .map((d) => {
                    return <ProcessEvent>{
                        eventType: 'process-event',
                        timestamp: diff.nextTimestamp,
                        action: d.next?.checked ? 'selected' : 'unselected',
                        selectionTarget: d.next?.metadataType!,
                        selectionTargetName: d.next?.label!,
                        explicit: true
                    };
                });
        }).flat();
    }

    private dimensionAndHierarchySelection(events: IEvent[], metadata: MetadataResponse) {
        // Get all display events from events
        const displayEvents = events.filter(e => e.eventType === 'display-event') as DisplayEvent[];
        // Get the mouse events
        const mouseEvents = events.filter(e => e.eventType === 'mouse-event') as MouseEvent[];
        // combine the display events and the mouse events with a similar timestamp
        const displayAndMouseEvents = displayEvents.map((de) => {
            return {
                displayEvent: de,
                mouseEvent: mouseEvents.filter(me => me.timestamp <= de.timestamp)
                    .sort((a, b) => a.timestamp - b.timestamp)
                    .at(-1)
            }
        });
        // Get the element over which the mouse was hovering
        const elementOverMouse = displayAndMouseEvents.map((dme) => {
            return {
                displayEvent: dme.displayEvent,
                mouseEvent: dme.mouseEvent,
                elements: dme.displayEvent.elements.filter(e => containsPoint(dme.mouseEvent, e) && e.metadataType)
            }
        }).map((dme) => {
            return {
                displayEvent: dme.displayEvent,
                mouseEvent: dme.mouseEvent,
                elements: dme.elements,
                // find smallest element
                smallestElement: dme.elements?.sort((a, b) => a.width * a.height - b.width * b.height)
                    .at(0)
            }
        }).filter(eom => eom.smallestElement)
            .sort((a, b) => a.displayEvent.timestamp - b.displayEvent.timestamp);
        // Check if the element is a dimension or a hierarchy
        return elementOverMouse.filter((eom) => eom.smallestElement?.metadataType === 'dimension' || eom.smallestElement?.metadataType === 'hierarchy' || eom.smallestElement?.metadataType === 'level')
            // and return the process event
            .map((eom) => {
                const explicitEvent: ProcessEvent = {
                    eventType: 'process-event',
                    timestamp: eom.displayEvent.timestamp,
                    action: 'selected',
                    selectionTarget: eom.smallestElement?.metadataType! as unknown as any,
                    selectionTargetName: eom.smallestElement?.label!,
                    explicit: true
                };
                const allEvents = [explicitEvent];
                if (explicitEvent.selectionTarget === 'hierarchy') {
                    // Implicitly selected dimension
                    const dimension = eom.elements.find(e => e.metadataType === 'dimension');
                    if (dimension) {
                        allEvents.push(<ProcessEvent>{
                            eventType: 'process-event',
                            timestamp: eom.displayEvent.timestamp,
                            action: 'selected',
                            selectionTarget: 'dimension',
                            selectionTargetName: dimension.label!,
                            explicit: false
                        });
                    }

                }
                if (explicitEvent.selectionTarget === 'dimension') {
                    // Implicitly selected hierarchy
                    const hierarchy = metadata.dimensions.find(d => d.name === explicitEvent.selectionTargetName)?.hierarchies[0];
                    if (hierarchy) {
                        allEvents.push(<ProcessEvent>{
                            eventType: 'process-event',
                            timestamp: eom.displayEvent.timestamp,
                            action: 'selected',
                            selectionTarget: 'hierarchy',
                            selectionTargetName: hierarchy.name,
                            explicit: false
                        });
                    }
                }
                if (explicitEvent.selectionTarget !== 'level') {
                    // Implicitly selected level: (All)
                    allEvents.push(<ProcessEvent>{
                        eventType: 'process-event',
                        timestamp: eom.displayEvent.timestamp,
                        action: 'selected',
                        selectionTarget: 'level',
                        selectionTargetName: '(All)',
                        explicit: false
                    });
                }

                return allEvents;
            }).flat();
    }

    private memberSelectionThroughZone(differential: DisplayDifferential[], events: IEvent[], selectionChanges: ProcessEvent[]) {
        const displayEvents = events.filter(e => e.eventType === 'display-event') as DisplayEvent[];
        return differential.map((diff) => {
            const displayEvent = <DisplayEvent | undefined>events.find(e => e.timestamp === diff.nextTimestamp && e.eventType === 'display-event');
            if (!displayEvent) {
                return [];
            }
            const memberSelectionZone = displayEvent.zones.find(z => z.label === 'Member Selection');
            if (!memberSelectionZone) {
                return [];
            }
            return diff.diffs.filter(d => d.type === 'add' && d.next?.metadataType && contains(d.next, memberSelectionZone) &&
                // There is no previously selected event that is not followed by an unselected event
                selectionChanges
                    .filter(sc => sc.selectionTargetName === d.next?.label && sc.timestamp <= diff.nextTimestamp)
                    .sort((a, b) => a.timestamp - b.timestamp).at(-1)?.action !== 'selected'
            ).map(d => {
                return <ProcessEvent>{
                    eventType: 'process-event',
                    timestamp: diff.nextTimestamp,
                    action: 'selected',
                    selectionTarget: this.memberOrAdHocMember(diff, displayEvents),// d.next?.metadataType!,
                    selectionTargetName: d.next?.label!,
                    explicit: true
                };
            })
        }).flat();
    }

    private memberOrAdHocMember(displayDifferential: DisplayDifferential, displayEvents: DisplayEvent[]) {
        // Check if a display event exists between the current and next timestamp
        const displayEvent = displayEvents.find(e => e.timestamp > displayDifferential.currentTimestamp && e.timestamp <= displayDifferential.nextTimestamp);
        // Check if the display event contains an object information that has the label 'Adhoc-Knoten erstellen'
        const adHocMember = displayEvent?.elements.find(e => e.label === 'Adhoc-Knoten erstellen');
        // if the adHocMember is not null, then the member is an adhoc member
        return adHocMember ? 'ad-hoc' : 'member';
    }

    private deselectionEvents(events: ProcessEvent[]) {
        const deselectionEvents: ProcessEvent[] = [];
        // if there is a dimension selection event and a dimension was previously selected, then the previous dimension is unselected
        const dimensionSelection = events.filter(e => e.selectionTarget === 'dimension' && e.action === 'selected')
            .sort((a, b) => a.timestamp - b.timestamp);
        // For each dimension selection event: after the first selection create a deselection event for the previous dimension
        dimensionSelection.forEach((ds, index) => {
            if (index > 0) {
                deselectionEvents.push(<ProcessEvent>{
                    eventType: 'process-event',
                    timestamp: ds.timestamp,
                    action: 'unselected',
                    selectionTarget: 'dimension',
                    selectionTargetName: dimensionSelection.at(index - 1)?.selectionTargetName,
                    explicit: false
                });
            }
        });
        // if there is a hierarchy selection event and a hierarchy was previously selected, then the previous hierarchy is unselected
        const hierarchySelection = events.filter(e => e.selectionTarget === 'hierarchy' && e.action === 'selected')
            .sort((a, b) => a.timestamp - b.timestamp);
        // For each hierarchy selection event: after the first selection create a deselection event for the previous hierarchy
        hierarchySelection.forEach((hs, index) => {
            if (index > 0) {
                deselectionEvents.push(<ProcessEvent>{
                    eventType: 'process-event',
                    timestamp: hs.timestamp,
                    action: 'unselected',
                    selectionTarget: 'hierarchy',
                    selectionTargetName: hierarchySelection.at(index - 1)?.selectionTargetName,
                    explicit: false
                });
            }
        });
        // if there is a level selection event and a level was previously selected, then the previous level is unselected
        const levelSelection = events.filter(e => e.selectionTarget === 'level' && e.action === 'selected')
            .sort((a, b) => a.timestamp - b.timestamp);
        // For each level selection event: after the first selection create a deselection event for the previous level
        levelSelection.forEach((ls, index) => {
            if (index > 0) {
                deselectionEvents.push(<ProcessEvent>{
                    eventType: 'process-event',
                    timestamp: ls.timestamp,
                    action: 'unselected',
                    selectionTarget: 'level',
                    selectionTargetName: levelSelection.at(index - 1)?.selectionTargetName,
                    explicit: false
                });
            }
        });
        return deselectionEvents;
    }
}